/***************************************************************************
 * This code is an original creation. It is intended to be used for a one  *
 * reader, one card situation within the Micro Framework. This is intended *
 * to be used to generate a new device class to support a reader chip not  *
 * already supported with this API.                                        *
 * LAST MODIFIED: 2010 April 23                                            *
 * Copyright (C) 2009 Gemalto                                              *
 ***************************************************************************/
/***************************************************************************
 *  Device.cs is part of the PC/SC Micro API for the .NET Micro            *
 *  Framework.                                                             *
 *                                                                         *
 *  The PC/SC Micro API for the .NET Micro Framework is free software:     *
 *  you can redistribute it and/or modify it under the terms of the GNU    *
 *  General Public License as published by the Free Software Foundation,   *
 *  either version 2 of the License, or (at your option) any later version.*
 *                                                                         *
 *  The PC/SC Micro API for the .NET Micro Framework is distributed in     *
 *  the hope that it will be useful, but WITHOUT ANY WARRANTY; without even*
 *  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR    *
 *  PURPOSE.  See the GNU General Public License for more details.         *
 *                                                                         *
 *  You should have received a copy of the GNU General Public License      *
 *  along with the PC/SC Micro API for the .NET Micro Framework. If not,   *
 *  see <http://www.gnu.org/licenses/>.                                    *
 ***************************************************************************/
using System;
using Microsoft.SPOT;

namespace PCSCMicro
{
    /// <summary>
    /// Skeleton device class for communicating with a smart card reader.
    /// </summary>
    /// <remarks>
    /// This is a skeleton device class for communicating directly to a 
    /// smart card reader. This class does little error checking, as most
    /// error checking is reader specific. Also, this class is missing 
    /// values for the Hard-coded configuration constants. In addition,
    /// the specific format of communicating with a reader is missing.
    /// For examples on all of the above, refer to the GemCore or the 
    /// Teridian Device classes. 
    /// </remarks>
    class Device : Reader
    {
        #region Definition of constants and global variables

        // Collecting Data TimeOut Design Setting
        int TestTimeOut;            // Timeout for APDU calculated to second
        int InitializationTimeOut;  // Apply to non-APDU and non-EMV testing command, in second
        int TimeOutUnitInMiliSec;   // This TimeOut is independent with the ones in Min and Sec

        // Hard-coded configuration constants
        // Change strings to values used by reader
        const string ACK = "ACK";
        const string NACK = "NACK";
        const string PowerOnReader = "PowerOnReader"; // Some readers do not have this command
        const string PowerOnSC = "PowerOnSC";
        const string PowerOffSC = "PowerOffSC";
        const string CheckStatus = "CheckStatus";
        const string SetParametersCommand = "SetParametersCommand";
        const string APDUCommand = "APDUCommand";
        const string RestartReader = "RestartReader"; // Some readers do not have this command

        // Keep track of slot status
        const int ACTIVATED = 0;
        const int PRESENT = 1;
        const int NOCARD = 2;
        const int ERROR = 3;
        int SlotStatus = NOCARD;

        // Maximum number of log lines
        const int MaxBytes = 4096;

        // Flags used for running processes
        bool IsSerialPortOn = false;
        bool APDUCommandComplete = true;
        bool ActivatingSmartCard = false;
        bool CardActivated = false;
        bool ResettingCard = false;
        
        // Retry counter for serial port communication
        int NumberOfRetries;
        
        // Serial Port
        string port = "COM1";
        int baudrate = 9600;
        System.IO.Ports.SerialPort ComPort = new System.IO.Ports.SerialPort(System.IO.Ports.Serial.COM1);

        // Packet parser
        PacketParser Packet = new PacketParser();

        // PPS Strings
        string Protocol = "00";//Protocol to be set by user later
        
        // Card characteristics
        ReaderError error = ReaderError.NONE;
        string atr = null;

        #endregion

        /// <summary>
        /// Open and initialize serial port connection
        /// </summary>
        /// <param name="baudrate">Baudrate of serial port</param>
        /// <param name="handShake">Handshake of serial port</param>
        void InitializeComPort(int baudrate, System.IO.Ports.Handshake handShake)
        {
            if (!ComPort.IsOpen)
            {
                // Set COM port properties
                ComPort.BaudRate = baudrate;
                ComPort.Parity = System.IO.Ports.Parity.None;
                ComPort.StopBits = System.IO.Ports.StopBits.One;
                ComPort.DataBits = 8;
                ComPort.Handshake = handShake;
                ComPort.ReadTimeout = 5000;
                ComPort.Open();
            }
        }

        /// <summary>
        /// Deal with errors from reader response
        /// </summary>
        /// <param name="packet">Packet created using reader response
        /// to use to deal with errors</param>
        void ResponseValidation(PacketParser packet)
        {
            // Infinite Loop detection 
                error = ReaderError.NO_RESPONSE;
            // Other error detection
                error = ReaderError.UNKNOWN_ERROR;
            // Correct data response
                error = ReaderError.NONE;
        }

        /// <summary>
        /// Detects completed package from reader
        /// </summary>
        /// <param name="byteArray">Response so far</param>
        /// <param name="length">Length of response so far</param>
        /// <returns>True if packet is complete, false otherwise</returns>
        bool IsComplete(byte[] byteArray, int length)
        {
            return false;
        }
        
        /// <summary>
        /// Send an array of bytes to the serial port
        /// </summary>
        /// <param name="Message">Message to send to serial port</param>
        /// <param name="Response">Place to put response from serial port</param>
        /// <param name="InputTimeOutInSec">Timeout value for serial port</param>
        void SendReceive(string Message, ref string Response, int InputTimeOutInSec)
        {
            int RetryNACK = NumberOfRetries;
        SendLabel:   // Used when receiving a NACK response, comand sending must be resent
            /*-----------------------------------*/
            /*     Send data to the Serial Port  */
            /*-----------------------------------*/

            #region Send data to the Serial Port
            //Convert Hex String to Byte Array
            byte[] myBuffer = new byte[Message.Length / 2]; // reserve 2 byte for CRC
            myBuffer = HexStringDecoder.StringToByteArray(Message);
            if ((Message != ACK) && (Message != NACK))
            {
                // Error detection and correction calculation
            }
            // Update Message to include Error detection bits
            Message = HexStringDecoder.ByteArrayToString(myBuffer, myBuffer.Length);
            ComPort.DiscardInBuffer();
            ComPort.DiscardOutBuffer();
            // Send single byte in the byte array so it can be parsed
            for (int i = 0; i < myBuffer.Length; i++)
            {
                ComPort.Write(myBuffer, i, 1);
            }
            #endregion

            /*-----------------------------------*/
            /* Receive data from the Serial Port */
            /*-----------------------------------*/

            #region Receive data from the Serial Port
            // Initialize receive related variables
            int TimeOutCycleCountDown = (InputTimeOutInSec * 1000 / TimeOutUnitInMiliSec);
            bool TimeOut = false;
            int bytecount = 0;
            byte[] byteArrayBuffer = new byte[MaxBytes];
            byte[] byteArrayReturn = new byte[MaxBytes];
            // Reset the data string received and the byte received
            Response = "";
            // Unnecessary to collect data if it's an ACK or NACK
            if ((Message == ACK) || (Message == NACK))
                return;
            // Do the loop if there's no timeout or the timeout cycle still exists
            while ((!TimeOut) && (TimeOutCycleCountDown > 0) && (bytecount < MaxBytes))
            {
                // Attempt to read from COM port
                if (1 == ComPort.Read(byteArrayBuffer, bytecount, 1))
                {
                    bytecount++;
                }
                else
                {
                    // set error and return
                    error = ReaderError.NO_RESPONSE;
                    return;
                }
                // Check if end of packet has been received
                if (IsComplete(byteArrayBuffer, bytecount))
                {
                    TimeOut = true;
                }
            }
            // Copy data to Response
            byteArrayReturn = byteArrayBuffer;
            Response = HexStringDecoder.ByteArrayToString(byteArrayReturn, bytecount);
            #endregion

            /*-----------------------------------*
             *        Handling NACK reponse      *
             *-----------------------------------*/

            #region Handling NACK reponse
            // Prepare to resend the command
            if ((Response != null) && (Response.Length >= NACK.Length) &&
                (Response.Substring(0, NACK.Length) == NACK) && (RetryNACK-- > 0))
            {
                // Terminate all the running command after a number of retries when receiving NACK response
                if (RetryNACK == 0)
                {
                    goto SendLabel;
                }
            }
            #endregion
        }
        
        /// <summary>
        /// Connect reader after calling Initialize Component
        /// </summary>
        /// <returns>True if reader connected, false otherwise</returns>
        internal override bool ConnectReader()
        {
            string CmdStr = PowerOnReader;
            try
            {
                InitializeComPort(baudrate, System.IO.Ports.Handshake.None);
                string response = "";
                SendReceive(CmdStr, ref response, InitializationTimeOut);
                Packet.GetPacket(response);
                ResponseValidation(Packet);
                if (Packet.ErrorCode == PacketParser.ErrorType.None)
                {
                    IsSerialPortOn = true;
                    return true;
                }
            }
            catch
            {
                if (ComPort.IsOpen)
                {
                    ComPort.Close();
                    return false;
                }
            }
            return false;
        }

        /// <summary>
        /// Disconnect reader and close serial port
        /// </summary>
        /// <returns>True if reader disconnected, false otherwise</returns>
        internal override bool DisconnectReader()
        {
            if(ComPort.IsOpen)
            {
                ComPort.Close();
            }
            IsSerialPortOn = false;
            return false;
        }

        /// <summary>
        /// Reset reader
        /// </summary>
        /// <returns>True if reader was reset, false otherwise</returns>
        internal override bool ResetReader()
        {
            string CmdStr = PowerOnReader;
            if (CmdStr == null || CmdStr.Equals(""))
            {
                DisconnectReader();
                return ConnectReader();
            }
            try
            {
                string response = "";
                SendReceive(CmdStr, ref response, InitializationTimeOut);
                Packet.GetPacket(response);
                ResponseValidation(Packet);
                if (Packet.ErrorCode == PacketParser.ErrorType.None)
                {
                    IsSerialPortOn = true;
                    return true;
                }
            }
            catch
            {
                if (ComPort.IsOpen)
                {
                    ComPort.Close();
                    return false;
                }
            }
            return false;
        }

        /// <summary>
        /// Update the smart card status of the reader
        /// </summary>
        void UpdateStatus()
        {
            // Create status command string
            string StatusCmdStr = CheckStatus;
            try
            {
                // Reset activated flag in case card is no longer activated
                CardActivated = false;
                // Send command to reader
                string Response = null;
                SendReceive(StatusCmdStr, ref Response, InitializationTimeOut);
                // Parse Response
                Packet.GetPacket(Response);
                ResponseValidation(Packet);
                // If error, card is in error mode
                if (Packet.ErrorCode != PacketParser.ErrorType.None)
                {
                    SlotStatus = ERROR;
                    error = ReaderError.UNKNOWN_ERROR;
                }
                else
                {
                    // Parse where Response holds status
                    switch (int.Parse(Response.Substring(0, 1))) 
                    {
                        // Decode card's response
                        case NOCARD:
                            atr = null;
                            SlotStatus = NOCARD;
                            break;
                        case PRESENT:
                            atr = null;
                            SlotStatus = PRESENT;
                            break;
                        case ACTIVATED:
                            SlotStatus = ACTIVATED;
                            CardActivated = true;
                            break;
                        default:
                            atr = null;
                            SlotStatus = ERROR;
                            return;
                    }
                }
                return;
            }
            catch
            {
                atr = null;
                SlotStatus = ERROR;
                error = ReaderError.UNKNOWN_ERROR;
                return;
            }
        }

        /// <summary>
        /// Return the smart card status of the reader
        /// </summary>
        /// <returns>Status of smart card in the form of an int.
        /// ACTIVATED = 0,
        /// PRESENT = 1,
        /// NOCARD = 2,
        /// ERROR = 3 </returns>
        internal override int GetStatus()
        {
            UpdateStatus();
            return SlotStatus;
        }

        /// <summary>
        /// Activate smart card
        /// </summary>
        /// <returns>True if smart card activated, false otherwise</returns>
        internal override bool ActivateCard()
        {
            // Set activation command string
            string Response = null;
            string ActivationCmdStr = PowerOnSC;
            try
            {
                // Set activation flag
                ActivatingSmartCard = true;
                // Send command to reader
                SendReceive(ActivationCmdStr, ref Response, InitializationTimeOut);
                // Parse response
                Packet.GetPacket(Response);
                ResponseValidation(Packet);
                // If no response, deactivate
                if (Packet.ErrorCode == PacketParser.ErrorType.NoResponse
                    || Packet.ErrorCode == PacketParser.ErrorType.NACK)
                {
                    string DeactivationCmdStr = PowerOffSC;

                    SendReceive(DeactivationCmdStr, ref Response, InitializationTimeOut);
                    Packet.GetPacket(Response);
                    ResponseValidation(Packet);

                    SlotStatus = ERROR;
                    error = ReaderError.NO_RESPONSE;
                    ActivatingSmartCard = false;
                    return false;
                }
                // Any other error, set status as error
                else if (Packet.ErrorCode != PacketParser.ErrorType.None)
                {
                    SlotStatus = ERROR;
                    error = ReaderError.UNKNOWN_ERROR;
                    ActivatingSmartCard = false;
                    return false;
                }
                // No error means card is activated
                else
                {
                    SlotStatus = ACTIVATED;
                    atr = Response.Substring(8, Response.Length - 2 - 8);
                }
                // Update activation flags
                ActivatingSmartCard = false;
                CardActivated = true;
                return true;
            }
            catch
            {
                SlotStatus = ERROR;
                error = ReaderError.UNKNOWN_ERROR;
                return false;
            }
        }

        /// <summary>
        /// Send PPS to the reader
        /// </summary>
        /// <returns>True if PPS successfully sent, false otherwise</returns>
        bool SendPPS()
        {
            string SetParametersCmdStr = SetParametersCommand + Protocol;
            try
            {
                // Send PPS to card
                string Response = null;
                SendReceive(SetParametersCmdStr, ref Response, InitializationTimeOut);
                // Parse Response
                Packet.GetPacket(Response);
                ResponseValidation(Packet);
                return true;
            }
            catch
            {
                error = ReaderError.UNKNOWN_ERROR;
                return false;
            }
        }

        /// <summary>
        /// Change protocol of the reader and call SendPPS
        /// </summary>
        /// <param name="protocol">Protocol to be sent</param>
        /// <returns>True if PPS successfully sent, false otherwise</returns>
        internal override bool SendPPS(byte protocol)
        {
            error = ReaderError.NONE;
            if (protocol != 1 && protocol != 0)
            {
                error = ReaderError.BAD_PROTOCOL;
                return false;
            }
            Protocol = HexStringDecoder.ByteToHex(protocol);
            return SendPPS();
        }

        /// <summary>
        /// Deactivate smart card
        /// </summary>
        /// <returns>True if smart card deactivated, false otherwise</returns>
        internal override bool DeactivateCard()
        {
            // Create deactivation command string
            string DeactivationCmdStr = PowerOffSC;
            try
            {
                // Set deactivating flag
                ActivatingSmartCard = true;
                // Send command to reader
                string Response = null;
                SendReceive(DeactivationCmdStr, ref Response, InitializationTimeOut);
                // Parse response
                Packet.GetPacket(Response);
                ResponseValidation(Packet);
                // If error, assume card was not deactivated
                if (Packet.ErrorCode != PacketParser.ErrorType.None)
                {
                    SlotStatus = ERROR;
                    atr = null;
                    error = ReaderError.UNKNOWN_ERROR;
                    ActivatingSmartCard = false;
                    CardActivated = false;
                    return false;
                }
                else
                {
                    SlotStatus = PRESENT;
                    atr = null;
                }
                // Reset flags
                ActivatingSmartCard = false;
                CardActivated = false;
                return true;
            }
            catch
            {
                ActivatingSmartCard = false;
                CardActivated = false;
                SlotStatus = ERROR;
                error = ReaderError.UNKNOWN_ERROR;
                return false;
            }
        }

        /// <summary>
        /// Reset smart card
        /// </summary>
        /// <returns>True if smart card was reset, false otherwise</returns>
        internal override bool ResetCard()
        {
            // Set reset flag
            ResettingCard = true;
            // Deactivate card if necessary, then reactivate
            if (CardActivated)
            {
                DeactivateCard();
            }
            bool ret = ActivateCard();
            ResettingCard = false;
            return ret;
        }

        /// <summary>
        /// Check to see if reader is connected
        /// </summary>
        /// <returns>True if reader is connected, false otherwise</returns>
        internal override bool IsReaderActive()
        {
            return IsSerialPortOn;
        }

        /// <summary>
        /// Send command in parameters directly to reader
        /// </summary>
        /// <param name="commandCode">Command code to send to reader</param>
        /// <param name="data">Data to complement the command code </param>
        /// <returns>Response from reader to command</returns>
        internal override string ExecuteCommand(string commandCode, string data)
        {
            // Create command string
            string CmdStr = commandCode + data;
            try
            {
                // Send command to reader
                string Response = null;
                SendReceive(CmdStr, ref Response, InitializationTimeOut);
                // Parse response
                Packet.GetPacket(Response);
                ResponseValidation(Packet);
                return Response;
            }
            catch
            {
                error = ReaderError.UNKNOWN_ERROR;
                return null;
            }
        }

        /// <summary>
        /// Get current error
        /// </summary>
        internal override ReaderError Error
        {
            get
            {
                return error;
            }
        }

        /// <summary>
        /// Get ATR
        /// </summary>
        internal override string ATR
        {
            get
            {
                return atr;
            }
        }

        /// <summary>
        /// Send APDU to smart card
        /// </summary>
        /// <param name="apduText">APDU to be sent to card</param>
        /// <returns>Response to APDU</returns>
        internal override string SendAPDU(string apduText)
        {
            APDUCommandComplete = false;
            string Response = null;
            string APDUCmdStr = APDUCommand + apduText;
            try
            {
                // Send APDU string
                SendReceive(APDUCmdStr, ref Response, TestTimeOut);
                // Parse Response string
                Packet.GetPacket(Response);
                ResponseValidation(Packet);
                Response = Packet.resp;
                APDUCommandComplete = true;
                return Response;
            }
            catch
            {
                APDUCommandComplete = true;
                SlotStatus = ERROR;
                error = ReaderError.UNKNOWN_ERROR;
                return null;
            }
        }
    }
}
